package xyz.lwm.lazycat.jdbc.exec;

import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import xyz.lwm.lazycat.jdbc.conf.JdbcProperties;
import xyz.lwm.lazycat.setting.Setting;
import xyz.lwm.lazycat.utility.Assert;
import xyz.lwm.lazycat.utility.CollectionUtil;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * implementation of JdbcOperator
 *
 * @author lwm
 */
@Slf4j
public class JdbcTemplate implements JdbcOperator {

    private static final boolean printSql = Setting.get().forPath(JdbcProperties.class).isPrintSql();

    private final DataSource dataSource;

    private Duration queryTimeout = Duration.ZERO;

    public JdbcTemplate(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public JdbcTemplate setQueryTimeout(@NotNull Duration timeout) {
        Assert.notNull(timeout, "timeout");
        this.queryTimeout = timeout;
        return this;
    }

    @Override
    public <T> T execute(@NotNull JdbcCallback<Connection, T> action) throws DataAccessException {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            return action.call(conn);
        } catch (SQLException | DataAccessException e) {
            log.error(e.getMessage());
            throw new DataAccessException(e.getMessage(), e);
        } finally {
            JdbcUtil.closeConnection(conn);
        }
    }

    @Override
    public <T> T execute(@NotNull PreparedStatementCreator psc, @NotNull JdbcCallback<PreparedStatement, T> action) throws DataAccessException {
        return execute(conn -> {
            PreparedStatement ps = null;
            try {
                ps = psc.createPreparedStatement(conn);
                ps.setQueryTimeout((int) queryTimeout.getSeconds());
                return action.call(ps);
            } catch (SQLException e) {
                throw new DataAccessException(e.getMessage(), e);
            } finally {
                JdbcUtil.closeStatement(ps);
            }
        });
    }

    @Override
    public <T> T query(@NotNull String sql, PreparedStatementSetter pss, @NotNull JdbcCallback<ResultSet, T> resultHandler) throws DataAccessException {
        Assert.notNull(sql, "sql cannot be null");
        if (printSql) {
            log.info("\nExecuting sql is: {}\n", sql);
        }

        return execute(conn -> conn.prepareStatement(sql), ps -> {
            if (pss != null) {
                pss.setValues(ps);
            }
            ResultSet rs = null;
            try {
                rs = ps.executeQuery();
                return resultHandler.call(rs);
            } catch (SQLException e) {
                throw new DataAccessException(e.getMessage(), e);
            } finally {
                JdbcUtil.closeResultSet(rs);
            }
        });
    }

    @NotNull
    @Override
    public <T> List<T> queryList(@NotNull String sql, @NotNull RowMapper<T> rowMapper, Object... args) throws DataAccessException {
        return Objects.requireNonNull(query(sql, new ArgPreparedStatementSetter(args), rs -> {
            List<T> result = new ArrayList<>();
            for (int i = 0; rs.next(); ) {
                result.add(rowMapper.mapRow(rs, ++i));
            }
            return result;
        }));
    }

    @NotNull
    @Override
    public <T> List<T> queryList(@NotNull String sql, @NotNull Class<T> elementType, Object... args) throws DataAccessException {
        return queryList(sql, RowMappers.getRowMapper(elementType), args);
    }

    @NotNull
    @Override
    public List<Map<String, Object>> queryListForMap(@NotNull String sql, Object... args) throws DataAccessException {
        return queryList(sql, new RowMappers.MapRowMapper(), args);
    }

    @Override
    public <T> T queryOne(@NotNull String sql, Class<T> elementType, Object... args) throws DataAccessException {
        List<T> list = queryList(sql, elementType, args);
        Assert.isTrue(list.size() <= 1, "query one result but found {}", list.size());
        return list.isEmpty() ? null : list.get(0);
    }

    @Override
    public Map<String, Object> queryOneForMap(@NotNull String sql, Object... args) throws DataAccessException {
        List<Map<String, Object>> list = queryListForMap(sql, args);
        Assert.isTrue(list.size() <= 1, "query one result but found {}", list.size());
        return list.isEmpty() ? null : list.get(0);
    }

    @Override
    public int update(@NotNull String sql, PreparedStatementSetter pss) throws DataAccessException {
        Assert.notNull(sql, "sql cannot be null");
        if (printSql) {
            log.info("\nExecuting sql is: {}\n", sql);
        }

        return Objects.requireNonNull(execute(conn -> conn.prepareStatement(sql), ps -> {
            if (pss != null) {
                pss.setValues(ps);
            }
            return ps.executeUpdate();
        }));
    }

    @Override
    public int update(@NotNull String sql, Object... args) throws DataAccessException {
        return update(sql, new ArgPreparedStatementSetter(args));
    }

    private static class ArgPreparedStatementSetter implements PreparedStatementSetter {
        private final Object[] args;
        private ArgPreparedStatementSetter(Object[] args) {
            this.args = args;
        }
        @Override
        public void setValues(@NotNull PreparedStatement ps) throws SQLException {
            if (CollectionUtil.isNotEmpty(args)) {
                ParameterMetaData pmd = ps.getParameterMetaData();
                for (int i = 0; i < args.length; i++) {
                    int index = i + 1;
                    Object arg = args[i];
                    JdbcUtil.setValue(ps, pmd, index, arg);
                }
            }
        }

    }

}
